﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Language.Scc;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using Red.Scripts.Defines.Utilities;

namespace Language
{
    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    {
        #region Data

        private ITextStructureNavigator _navigator;
        private Stub.Database _database;
        private Parser.Scc _parser;
        private ITextSnapshot _taggedSnapshot;

        #endregion Data

        #region C-Tor

        public OutliningTagger( ITextBuffer buffer, Connection connection, Stub.Database database, ITextStructureNavigator navigator )
        {
            _database = database;
            _navigator = navigator;

            _parser = new Parser.Scc( buffer, connection, _database );
            _parser.ParseComplete += _parser_ParseComplete;
        }

        #endregion C-Tor

        #region Private Methods
        
        private IEnumerable<Stub.IScope> FindScopes( SortedList<int, Stub.IScope> scopes, int start, int end )
        {
            int index = scopes.LowerBound( start );

            if ( index == -1 )
                yield break;

            while( index < scopes.Count && scopes.Values[ index ].Scope.Start.Position.Start < end )
            {
                yield return scopes.Values[ index ];

                ++index;
            }
        }

        private IEnumerable<ITagSpan<IOutliningRegionTag>> BuildTags( SnapshotSpan span, SortedList<int, Stub.IScope> scopes )
        {
            IEnumerable<Stub.IScope> applicableScopes = FindScopes( scopes, span.Start.Position, span.End.Position );

            foreach( Stub.IScope scope in applicableScopes )
            {
                var tag = BuildTag( span, scope );

                if ( tag != null )
                    yield return tag;
            }
        }

        private int ReverseEatWhitespace( ITextSnapshot snapshot, int start )
        {
            if( start > 1 )
            {
                TextExtent preceedingWhitespace = _navigator.GetExtentOfWord( new SnapshotPoint( snapshot, start - 1 ) );

                // IsSignificant == true if non-whitespace
                if ( !preceedingWhitespace.IsSignificant )
                {
                    start = preceedingWhitespace.Span.Start.Position;

                    // We recurse to cover any newlines, that seem to prevent GetExtentOfWord() from enclosing all the whitespace between the stub declaration and the scope (body)
                    return ReverseEatWhitespace( snapshot, start );
                }
            }

            return start;
        }

        private ITagSpan<IOutliningRegionTag> BuildTag( SnapshotSpan span, Stub.IScope scope )
        {
            if ( _taggedSnapshot == null )
                return null;

            Stub.PositionContext taggedRegion = new Stub.PositionContext() { Start = scope.Scope.Start.Position.Start, End = scope.Scope.End.Position.End };

            if ( taggedRegion == null )
                return null;

            Stub.PositionContext previewRegion = taggedRegion;

            if ( previewRegion == null )
                return null;

            int start = ReverseEatWhitespace( _taggedSnapshot, taggedRegion.Start );
            int end = taggedRegion.End;
            int length = end - start;

            // Cut out the surrounding brackets for the preview
            int previewStart = previewRegion.Start;
            int previewEnd = previewRegion.End;
            int previewLength = previewEnd - previewStart;

            if( previewStart + previewLength > _taggedSnapshot.Length )
            {
                System.Diagnostics.Debug.Print( $"Error: Tried to get an invalid text range from a snapshot: {previewStart}-{previewLength} but the snapshot is only {_taggedSnapshot.Length} in size" );

                return null;
            }

            string preview = _taggedSnapshot.GetText( previewStart, previewLength ).Trim( ' ', '\n', '\r' );

            preview = preview.Substring( 0, Math.Min( preview.Length, 400 ) );

            Span target = new Span( start, length );

            string collapsedForm = ( scope as Stub.NameContext ).Name ?? scope.GetType().Name;
            System.Diagnostics.Debug.Assert( collapsedForm != null );

            return new TagSpan<IOutliningRegionTag>
            (
                new SnapshotSpan( _taggedSnapshot, target ),
                new OutliningRegionTag
                (
                    false,
                    false,
                    collapsedForm,
                    preview
                )
            );
        }

        #endregion Private Methods

        #region Event Handlers

        private void _parser_ParseComplete( SnapshotSpan snapshotSpan, ITextSnapshot snapshot )
        {
            System.Diagnostics.Debug.Print( $"[Outlining tagger] Database updated with version {snapshot.Version} (Current version: {_parser.Snapshot.Version}" );

            _taggedSnapshot = snapshot;

            TagsChanged?.Invoke( this, new SnapshotSpanEventArgs( snapshotSpan ) );
        }

        #endregion Event Handlers

        #region ITagger<IOutliningRegionTag>

        public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
        
        public IEnumerable<ITagSpan<IOutliningRegionTag>> GetTags( NormalizedSnapshotSpanCollection spans )
        {
            HashSet<Stub.Context> added = new HashSet<Stub.Context>();

            Stub.File fileStub;
            if( _database.FileByPath.TryGetValue( _parser.FilePath.Relative, out fileStub ) )
            {
                foreach ( var span in spans )
                {
                    var tags = BuildTags( span, fileStub.Scopes );

                    foreach ( var tag in tags )
                        yield return tag;
                }
            }
        }

        #endregion ITagger<IOutliningRegionTag>
    }

    #region Provider

    [Export( typeof( ITaggerProvider ) )]
    [TagType( typeof( IOutliningRegionTag ) )]
    [ContentType( "redscript" )]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    {
        [Import]
        private ITextStructureNavigatorSelectorService NavigatorService { get; set; }

        [Import]
        public Connection Connection { get; set; }

        [Import]
        public Stub.Database Database { get; set; }

        #region ITaggerProvider

        public ITagger<T> CreateTagger<T>( ITextBuffer buffer ) where T : ITag
        {
            //create a single tagger for each buffer.
            Func<ITagger<T>> sc = delegate () { return new OutliningTagger( buffer, Connection, Database, NavigatorService.GetTextStructureNavigator( buffer ) ) as ITagger<T>; };
            return buffer.Properties.GetOrCreateSingletonProperty( sc );
        }

        #endregion ITaggerProvider
    }

    #endregion Provider
}
